﻿//     Copyright (c) Microsoft Corporation.  All rights reserved.

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Net;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;
using System.Web;
using System.Web.Mvc;
using Gateway.Console.Models;
using Gateway.Console.Services;
using Gateway.Console.Services.Azure;
using Microsoft.Gateway.Utils;
using Microsoft.Gateway.Utils.Update;
using Microsoft.IdentityModel.Clients.ActiveDirectory;
using Microsoft.WindowsAzure;
using Microsoft.WindowsAzure.Subscriptions;

namespace Gateway.Console.Controllers
{
    public class DeploymentController : OAuthAccessController
    {
        private readonly int _svcPkgCopyStatusMaxRetries        = Configuration.SvcPkgCopyStatusMaxRetries;
        private readonly int _svcPkgCopyStatusSleepPeriod       = Configuration.SvcPkgCopyStatusSleepPeriod;
        
        public DeploymentController()
            : base("Deployment", new[] { ResourceSelectors.RDFE, ResourceSelectors.GraphApi  })
        {
        }

        public async Task<ActionResult> Index()
        {
            return await DoWithAccessAsync("Index", async () =>
            {
                return View(await _db.Deployments.ToListAsync());
            });
        }

        [HttpGet]
        public async Task<ActionResult> GetDeploymentUpdates()
        {
            var client = new UpdateClient(this.Request.ServerVariables["SERVER_NAME"]);
            var manifests = await client.GetAvailableManifestsAsync(UpdateClient.Components.Gateway);
            var deployments = await _db.Deployments.ToListAsync();
            return Json(deployments
                    .Where(deployment => !deployment.OperationInProgress && !String.IsNullOrWhiteSpace(deployment.DeployedVersion))
                    .Select(deployment =>
                    {
                        Version deploymentVersion;
                        VersionUtils.TryParseVersion(deployment.DeployedVersion, out deploymentVersion);
                        string packageName = deployment.IsHttpsEnabled ? FilePackage.PackageNameHttps : FilePackage.PackageNameHttp;
                        var applicableUpdates = manifests
                            .Where(update => !deployment.OperationInProgress && 
                                deploymentVersion != null && 
                                update.Version > deploymentVersion &&
                                update.GetPackage(packageName) != null)
                            .ToList();
                        return new
                        {
                            DeploymentId = deployment.DeploymentId,
                            AvailableUpdate = applicableUpdates
                                .OrderByDescending(update => update.Severity)
                                .Select(manifest => new 
                                { 
                                    Version = manifest.Version.GatewayFormat(),
                                    Severity = manifest.Severity,
                                    Description = manifest.Description,
                                })
                                .FirstOrDefault(),
                        };
                    })
                    .Where(deployment => deployment.DeploymentId != 0 && deployment.AvailableUpdate != null),
                JsonRequestBehavior.AllowGet);
        }

        public async Task<ActionResult> Create()
        {
            return await DoWithAccessAsync("Create", async () =>
            {
                if (!ViewBag.RdfeAuthorizationFailure && !ViewBag.GraphApiAuthorizationFailure)
                {
                    try
                    {
                        var client = new SubscriptionClient(new TokenCloudCredentials(Session.GetAccessToken(ResourceSelectors.RDFE).AccessToken), AzureEnvironment.RdfeManagementEndpointUri);
                        var response = await client.Subscriptions.ListAsync();
                        var activeSubscriptions = response.Subscriptions
                            .Where(subscription => subscription.SubscriptionStatus == Microsoft.WindowsAzure.Subscriptions.Models.SubscriptionStatus.Active)
                            .ToList();

                        var deployment = new Deployment()
                        {
                            ServiceName = "",
                            Subscriptions = activeSubscriptions,
                            InstanceCount = 2,
                            NoSubscriptions = !activeSubscriptions.Any(),
                        };

                        if (deployment.Subscriptions.Any())
                        {
                            deployment.SubscriptionSelection = deployment.Subscriptions.First().SubscriptionId;
                            deployment.Regions = await GetRegions(deployment.SubscriptionSelection);
                        }
                        deployment.StorageAccounts = (await GetStorageAccountsForSubscription(deployment.SubscriptionSelection));
                        if (deployment.StorageAccounts.Any())
                        {
                            deployment.SelectedStorageAccount = deployment.StorageAccounts
                                .Select(account => account.Item1)
                                .FirstOrDefault();
                        }

                        return View(deployment);
                    }
                    catch (CloudException ex)
                    {
                        if (ex.ErrorCode == "ForbiddenError")
                        {
                            return View(new Deployment
                                {
                                    NoSubscriptions = true,
                                });
                        }
                        throw;
                    }
                }
                else
                {
                    return View(new Deployment());
                }
            });
        }

        public async Task<ActionResult> Delete(int id)
        {
            if (id == 0)
            {
                return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
            }
            return await DoWithAccessAsync("Delete", async () =>
            {
                var deployment = await _db.Deployments.FindAsync(id);
                if (deployment == null)
                {
                    return HttpNotFound();
                }
                return View(deployment);
            });
        }

        [HttpPost, ActionName("Delete")]
        [ValidateAntiForgeryToken]
        public async Task<ActionResult> DeleteConfirmed(int id)
        {
            return await DoWithAccessAsync("DeleteConfirmed", async () =>
            {
                Deployment deployment = await _db.Deployments.FindAsync(id);
                using (var deploymentManager = new DeploymentManager(Session.GetAccessToken(ResourceSelectors.RDFE), deployment.SubscriptionSelection))
                {
                    deploymentManager.DeleteDeployment(deployment.ServiceName);
                }

                _db.Deployments.Remove(deployment);
                await _db.SaveChangesAsync();

                return RedirectToAction("Index");
            });
        }

        [HttpGet]
        public async Task<ActionResult> CheckServiceNameAvailable(Deployment deployment)
        {
            using (var deploymentManager = new DeploymentManager(Session.GetAccessToken(ResourceSelectors.RDFE), deployment.SubscriptionSelection))
            {
                try
                {
                    var nameCheck = await deploymentManager.CheckServiceNameAvailableAsync(deployment.ServiceName);
                    if (nameCheck.Item1)
                    {
                        return Json(true, JsonRequestBehavior.AllowGet);
                    }
                    return Json(String.Format("The service: {0} is invalid. {1}", deployment.ServiceName, nameCheck.Item2), JsonRequestBehavior.AllowGet);
                }
                catch 
                {
                    return Json(String.Format("The service: {0} is invalid.", deployment.ServiceName), JsonRequestBehavior.AllowGet);
                }
            }
        }

        [HttpGet]
        public async Task<ActionResult> GetStorageAccounts(string selectedSubscriptionId)
        {            
            var storageAccounts = await GetStorageAccountsForSubscription(selectedSubscriptionId);         
            return Json(storageAccounts, JsonRequestBehavior.AllowGet);
        }

        [HttpGet]
        public async Task<ActionResult> GetSubscriptionRegions(string selectedSubscriptionId)
        {
            var subscriptionRegions = await GetRegions(selectedSubscriptionId);
            return Json(subscriptionRegions, JsonRequestBehavior.AllowGet);
        }

        [HttpPost]
        public async Task<ActionResult> Create(Deployment deployment)
        {
            if (!ModelState.IsValid)
            {
                var errors = string.Join("<br /> ", ModelState.Values.SelectMany(x => x.Errors).Select(x => x.ErrorMessage));
                return new HttpStatusCodeResult(HttpStatusCode.BadRequest, string.Format("Model does not satisfy validation: <br/> {0}", errors));                
            }
            return await UpdateModelAndDeploy(deployment, String.Empty);
        }

        public async Task<ActionResult> UpdateVersion(int id)
        {
            if (id == 0)
            {
                return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
            }
            return await DoWithAccessAsync("UpdateVersion", async () =>
            {
                try
                {
                    var deployment = await _db.Deployments.FindAsync(id);
                    if (deployment == null)
                    {
                        return HttpNotFound();
                    }
                    var client = new UpdateClient(this.Request.ServerVariables["SERVER_NAME"]);
                    var manifests = await client.GetAvailableManifestsAsync(UpdateClient.Components.Gateway);
                    Version deploymentVersion;
                    VersionUtils.TryParseVersion(deployment.DeployedVersion, out deploymentVersion);
                    string packageName = deployment.IsHttpsEnabled ? FilePackage.PackageNameHttps : FilePackage.PackageNameHttp;
                    this.ViewBag.AvailableUpdates = manifests
                        .Where(update => deploymentVersion == null || update.Version > deploymentVersion && 
                            update.GetPackage(packageName) != null)
                        .ToList();
                    this.ViewBag.PreviousUpdates = manifests
                        .Where(update => deploymentVersion != null && update.Version <= deploymentVersion &&
                            update.GetPackage(packageName) != null)
                        .ToList();
                    return View(deployment);
                }
                catch (Exception ex)
                {
                    return new HttpStatusCodeResult(HttpStatusCode.InternalServerError, ex.ToString());
                }
            });
        }

        [HttpPost]
        public async Task<ActionResult> UpdateVersion(int id, string version)
        {
            return await DoWithAccessAsync("UpdateVersion", async () =>
            {
                return await UpdateModelAndDeploy(await _db.Deployments.FindAsync(id), version);
            });
        }

        [HttpGet]
        public  ActionResult GetDeploymentStatus(int deploymentId)
        {
            var deploymentStatus = _db.DeploymentStatus.Where(d => d.DeploymentId == deploymentId).OrderByDescending( d => d.DateAdded).ToList();
            return Json(deploymentStatus, JsonRequestBehavior.AllowGet);
        }

        private async Task<ActionResult> DoWithAccessAsync(string actionName, Func<Task<ActionResult>> controllerAction)
        {
            return await DoWithAccessAsync(actionName,
                (selector, errorCode, errorDescription) =>
                {
                    if (selector == ResourceSelectors.GraphApi)
                    {
                        // This is only a problem if authN is enabled for the Gateway
                        var securityModel = _db.SecurityModels.FirstOrDefault();
                        if (securityModel != null && securityModel.EnableAuthenticationValue)
                        {
                            ViewBag.GraphApiAuthorizationFailure = true;
                        }
                    }
                    else
                    {
                        DefaultAccessFailureHandler(selector, errorCode, errorDescription);
                    }
                },
                controllerAction);
        }

        private async Task<List<string>> GetRegions(string selectedSubscriptionId)
        {
            using (var deploymentManager = new DeploymentManager(Session.GetAccessToken(ResourceSelectors.RDFE), selectedSubscriptionId))
            {
                var regions = await deploymentManager.GetSubscriptionRegionsAsync();
                return regions.Locations.Select(l => l.Name).ToList();            
            }
        }

        private async Task<IEnumerable<Tuple<string, string>>> GetStorageAccountsForSubscription(string selectedSubscriptionId)
        {
            using (var deploymentManager = new DeploymentManager(Session.GetAccessToken(ResourceSelectors.RDFE), selectedSubscriptionId))
            {
                IEnumerable<Tuple<string, string>> retval = new List<Tuple<string, string>> 
                    {
                        Tuple.Create(Deployment.CreateAccountTag, "Create New"),
                        Tuple.Create(Deployment.DividerTag, "----------"),
                    };
                try
                {
                    var storageAccounts = await deploymentManager.GetStorageAccountsAsync();
                    retval = retval.Concat(storageAccounts.Select(account => Tuple.Create(account, account)));
                }
                catch
                {
                }
                return retval;
            }
        }

        private async Task<ActionResult> UpdateModelAndDeploy(Deployment deployment, string version)
        {
            try
            {
                var graphCreds = Session.GetAccessToken(ResourceSelectors.GraphApi);
                var rdfeCreds = Session.GetAccessToken(ResourceSelectors.RDFE);
                var updateClient = new UpdateClient(this.Request.ServerVariables["SERVER_NAME"],
                    Configuration.PackageUpdateServiceLocation);
                PackageManifest deploymentManifest = null;
                if (String.IsNullOrWhiteSpace(version))
                {
                    // No version specified - get the latest one
                    deploymentManifest = await updateClient.GetLatestReleaseAsync(UpdateClient.Components.Gateway);
                }
                else
                {
                    deploymentManifest = await updateClient.GetUpdateVersionAsync(UpdateClient.Components.Gateway, version);
                }
                if (deploymentManifest == null)
                {
                    return HttpNotFound("Requested version was not found");
                }
                string packageName = deployment.IsHttpsEnabled ? FilePackage.PackageNameHttps : FilePackage.PackageNameHttp;
                var deploymentPackage = deploymentManifest.GetPackage(packageName);
                if (deploymentPackage == null)
                {
                    return new HttpStatusCodeResult(HttpStatusCode.InternalServerError, String.Format("Gateway manifest [{0}] does not contain package [{1}].",
                        deploymentManifest.Version.GatewayFormat(), packageName));
                }
                deployment.ServiceConfigTemplateLocation = updateClient.GetPackageFileUri(UpdateClient.Components.Gateway,
                    deploymentManifest, deploymentPackage, deploymentPackage.FindFileByExtension(".cscfg")).ToString();
                deployment.ServicePackageFileLocation = updateClient.GetPackageFileUri(UpdateClient.Components.Gateway,
                    deploymentManifest, deploymentPackage, deploymentPackage.FindFileByExtension(".cspkg")).ToString();
                // Prevent Gateway getting ahead of this console wrt config version
                var configUri = new ApiUriBuilder(Request.Url.GetLeftPart(UriPartial.Authority));
                configUri.Path = "/api/roleApi";
                deployment.RoleIndexConfigLocation = configUri.Uri.ToString();
                if (deployment.IsHttpsEnabled && deployment.Certificate != null)
                {
                    deployment.CertificateContents = await GetCertRawBytes(deployment.Certificate);
                    deployment.CertificateThumbprint = GetThumbprint(deployment.CertificateContents, deployment.CertificatePassword);
                }
                deployment.RefreshToken = rdfeCreds.RefreshToken;
                deployment.OperationInProgress = true;
                if (deployment.DeploymentId == 0)
                {
                    _db.Deployments.Add(deployment);
                }
                else
                {
                    _db.DeploymentStatus.RemoveRange(deployment.DeploymentStatus);
                    _db.Entry(deployment).State = System.Data.Entity.EntityState.Modified;
                }
                deployment.DeploymentStatus.Add(new DeployStatus(string.Format("Starting deployment process for '{0}', version: {1}", 
                    deployment.ServiceName, deploymentManifest.Version.GatewayFormat())));
                await _db.SaveChangesAsync();

                var authConfig = await _db.SecurityModels.FirstOrDefaultAsync();
                var deployTask = Task.Factory.StartNew(async () => await DoDeploymentAsync(deployment,
                                                                                           rdfeCreds,
                                                                                           authConfig,
                                                                                           graphCreds));

                return Json(deployment.DeploymentId);
            }
            catch (Exception ex)
            {
                return new HttpStatusCodeResult(HttpStatusCode.InternalServerError, ex.ToString());
            }
        }

        private async Task DoDeploymentAsync(Deployment deployment, AuthenticationResult rdfeToken, SecurityModel authConfig, AuthenticationResult graphToken)
        {
            // If Auth is enabled, verify/create/update the application in AAD
            if (authConfig != null && authConfig.EnableAuthenticationValue && graphToken != null)
            {
                try
                {
                    bool updateModel = false;
                    using (var graphApi = new AadGraphApi(graphToken.TenantId, graphToken))
                    {
                        if (String.IsNullOrWhiteSpace(authConfig.ApplicationName))
                        {
                            authConfig.ApplicationName = deployment.ServiceUrl;
                            updateModel = true;
                        }
                        await DeploymentManager.UpdateDeploymentProgressAsync(deployment, new DeployStatus("Configuring AAD for Gateway authentication"));
                        await graphApi.EnableWebSSOAsync(authConfig.ApplicationName, authConfig.MultiTenantAuthenticationValue, deployment.ServiceUrl);
                    }
                    if (updateModel)
                    {
                        using (var db = new ConsoleContext())
                        {
                            db.SecurityModels.Attach(authConfig);
                            db.Entry(authConfig).State = System.Data.Entity.EntityState.Modified;
                            await db.SaveChangesAsync();
                        }
                    }
                }
                catch (Exception ex)
                {
                    DeploymentManager.UpdateDeploymentProgress(deployment, new DeployStatus
                    {
                        Message = String.Format("Failed to update AAD for Gateway authentication. Details: {0}", ex),
                        Success = false,
                    });
                    return;
                }
            }

            using (var deploymentManager = new DeploymentManager(rdfeToken, deployment.SubscriptionSelection,
                    _svcPkgCopyStatusMaxRetries, TimeSpan.FromMilliseconds(_svcPkgCopyStatusSleepPeriod)))
            {
                await deploymentManager.DeployCloudServiceAsync(deployment);
            }

            //Update deployment with final config
            using (var db = new ConsoleContext())
            {
                deployment.OperationInProgress = false;
                db.Deployments.Attach(deployment);
                db.Entry(deployment).State = System.Data.Entity.EntityState.Modified;
                var res = await db.SaveChangesAsync();
            }
        }

        private async Task<byte[]> GetCertRawBytes(HttpPostedFileBase certificate)
        {
            using (var stream = certificate.InputStream)
            {
                var rawCert = new byte[stream.Length];
                await stream.ReadAsync(rawCert, 0, rawCert.Length);
                return rawCert;
            }
        }

        private string GetThumbprint(byte[] rawCert, string certPassword)
        {
            var x509 = new X509Certificate2(rawCert, certPassword, X509KeyStorageFlags.MachineKeySet);
            return x509.Thumbprint;
        }    
    }
}
